import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import org.apache.commons.collections.CollectionUtils;

import com.google.gdata.client.calendar.CalendarQuery;
import com.google.gdata.client.calendar.CalendarService;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.calendar.CalendarEventEntry;
import com.google.gdata.data.calendar.CalendarEventFeed;
import com.google.gdata.data.extensions.When;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;

/**
 * A convenience wrapper of Google's calendar service class
 * 
 * @author mkvalsvik
 * 
 */
public class GoogleCalendarService {

	private CalendarService service;
	private String googleAccount;

	public GoogleCalendarService(String googleAccount, String googlePassword)
			throws AuthenticationException {
		this.googleAccount = googleAccount;
		service = new CalendarService("inspectorTime-sync-1.0");
		service.setUserCredentials(googleAccount, googlePassword);
		service.useSsl();
	}

	public void create(SingleOccurrenceEvent event) {

		URL postUrl;
		try {
			postUrl = new URL("http://www.google.com/calendar/feeds/"
					+ this.googleAccount + "/private/full");
		} catch (MalformedURLException e1) {
			throw new RuntimeException(
					"Could not create google calendar entry", e1);
		}
		CalendarEventEntry myEntry = new CalendarEventEntry();

		myEntry.setTitle(new PlainTextConstruct(event.getTitle()));
		myEntry.setContent(new PlainTextConstruct(event.getDescription()));

		DateTime startTime = new DateTime(event.getStart());
		DateTime endTime = new DateTime(event.getEnd());
		When eventTimes = new When();
		eventTimes.setStartTime(startTime);
		eventTimes.setEndTime(endTime);
		myEntry.addTime(eventTimes);

		// Send the request and receive the response:
		try {
			service.insert(postUrl, myEntry);
		} catch (IOException e) {
			throw new RuntimeException(
					"Could not create google calendar entry", e);
		} catch (ServiceException e) {
			throw new RuntimeException(
					"Could not create google calendar entry", e);
		}
	}

	public List<SingleOccurrenceEvent> retrieveEvents(
			Date queryStart, Date queryEnd) {
		URL feedUrl;
		try {
			feedUrl = new URL(
					"http://www.google.com/calendar/feeds/" + googleAccount + "/private/full");
		} catch (MalformedURLException e1) {
			throw new RuntimeException("Could not retrieve calendar events", e1);
		}

		CalendarQuery myQuery = new CalendarQuery(feedUrl);
		myQuery.setMinimumStartTime(new DateTime(queryStart));
		myQuery.setMaximumStartTime(new DateTime(queryEnd));

		// Send the request and receive the response:
		CalendarEventFeed resultEventFeed;
		try {
			resultEventFeed = service.query(myQuery,
					CalendarEventFeed.class);
		} catch (IOException e) {
			throw new RuntimeException("Could not retrieve calendar events", e);
		} catch (ServiceException e) {
			throw new RuntimeException("Could not retrieve calendar events", e);
		}

		// Loop through resulting entries
		List<SingleOccurrenceEvent> events = new ArrayList<SingleOccurrenceEvent>();
		for (CalendarEventEntry entry : resultEventFeed.getEntries()) {
			
			for (When when : entry.getTimes()) {

				// Obtain time info				
				Date start = toTimeZoneAdjustedDate(when.getStartTime());
				Date end = toTimeZoneAdjustedDate(when.getEndTime());
				Calendar startCal = Calendar.getInstance();
				startCal.setTime(start);
				Calendar endCal = Calendar.getInstance();
				endCal.setTime(end);
				
				// Ignore timeslots that start and end at the same time
				if (start.getTime() == end.getTime()) {
					continue;
				}

				// Multi-day time?
				int startDay = startCal.get(Calendar.DAY_OF_YEAR);
				int endDay = endCal.get(Calendar.DAY_OF_YEAR);
				if (startDay != endDay) {
					
					// Yes we have a multi-day time. Break the time into multiple events, one for each day
					int cursorOffset = 0;
					Calendar firstTimeOfDayCal = Calendar.getInstance();
					firstTimeOfDayCal.set(Calendar.HOUR_OF_DAY, 00);
					firstTimeOfDayCal.set(Calendar.MINUTE, 00);
					firstTimeOfDayCal.set(Calendar.SECOND, 00);
					firstTimeOfDayCal.set(Calendar.MILLISECOND, 0);
					Calendar lastTimeOfDayCal = Calendar.getInstance();
					lastTimeOfDayCal.set(Calendar.HOUR_OF_DAY, 23);
					lastTimeOfDayCal.set(Calendar.MINUTE, 59);
					lastTimeOfDayCal.set(Calendar.SECOND, 59);
					lastTimeOfDayCal.set(Calendar.MILLISECOND, 999);
					Calendar cursorCal = Calendar.getInstance();
					for (cursorCal.setTime(startCal.getTime()); cursorCal.before(endCal); cursorCal.add(Calendar.DAY_OF_YEAR, 1)) {
						int cursorDay = cursorCal.get(Calendar.DAY_OF_YEAR);
						cursorOffset = cursorDay - startDay + 1;
						String modifiedTitle = entry.getTitle().getPlainText() + " [day " + cursorOffset + "]";
			
						
						// Create event for this day's portion of this multi-day time
						SingleOccurrenceEvent event = new SingleOccurrenceEvent();
						if (cursorDay == startDay) {
							event.setStart(start);	
						} else {
							
							// Move event start to midnight
							firstTimeOfDayCal.set(Calendar.DAY_OF_YEAR, cursorDay);
							event.setStart(firstTimeOfDayCal.getTime());
						}
						if (cursorDay == endDay) {
							event.setEnd(end);
						} else {
							
							// Move event end time to a millisecond before midnight
							lastTimeOfDayCal.set(Calendar.DAY_OF_YEAR, cursorDay);
							event.setEnd(lastTimeOfDayCal.getTime());
						}						
						event.setTitle(modifiedTitle);
						event.setDescription(entry.getPlainTextContent());
						events.add(event);	
					} 
				} else {
					
					// No we don't have a multi-day time 
					SingleOccurrenceEvent event = new SingleOccurrenceEvent();
					event.setStart(start);
					event.setEnd(end);
					event.setTitle(entry.getTitle().getPlainText());
					event.setDescription(entry.getPlainTextContent());
					events.add(event);		
				}				
			}
		}
		
		Collections.sort(events);
		
		return events;
	}

	/**
	 * @see http://code.google.com/p/gdata-java-client/source/browse/trunk/java/src/com/google/gdata/data/DateTime.java
	 * @param when
	 * @return
	 */
	private static Date toTimeZoneAdjustedDate(DateTime googleDateTime) {
		
		// Don't convert values that have timezone offset in them (they're already in the "local" timezone for this user)
		if (googleDateTime.getTzShift() != null) {
			Date date = new Date(googleDateTime.getValue());
			return date;
		}
		
		// If we don't have a timezone offset then we need to convert from GMT to local time ourselves 
		Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		long initialValue = googleDateTime.getValue();		
		cal.setTimeInMillis(initialValue);
		
		// Convert to local time by subtracting the GMT offset for the current timezone
		TimeZone zone = TimeZone.getDefault();
		int offset = zone.getOffset(cal.getTimeInMillis());
		long adjustedValue = initialValue - offset;
		Date date = new Date(adjustedValue);
		return date;
	}
}
